package model.code.line;

import event.TextReplacementEvent;
import model.symbol.Symbol;
import observer.ObservedString;
import util.ASUtil;
import util.ArrayUtil;

/**
 * All subclasses of this class represent specific types of lines.
 *
 * @author Neil Dickson
 */
public abstract class Line implements Comparable {
	/**
	 * The {@link LineList} containing this {@link Line}.
	 */
	protected LineList		list;

	protected char[]		text;
	protected int			length;

	protected Object[]		references;
	protected int[]			referenceIndices;
	protected boolean[]		isDefiningReference;
	protected int			nReferences;
	
	protected SyntaxError[]	errors;
	protected int			nErrors;

	private static final String[] SPACES_FOR_TAB = {"    ","   ","  "," "};

	private static final char[]	DEFAULT_EXTRA_WORD_CHARS = {'_'};

	public static final char[]	EMPTY_CHAR_ARRAY		= new char[0];

	public static final Object	WHITE_SPACE_MARKER		= new Object();
	public static final Object	OPERATOR_MARKER			= new Object();
	public static final Object	STRING_LITERAL_MARKER	= new Object();

	/**
	 * @return the text representation of the Line
	 */
	public String toString() {
		return new String(text,0,length);
	}

	/**
	 * @return the text representation of the Line
	 */
	public char[] toCharArray() {
		return ASUtil.subCharArray(text,0,length);
	}

	/**
	 * @param startCol
	 * @param endCol
	 * @return a substring of the text representation of the Line
	 */
	public String toString(int startCol,int endCol) {
		startCol = Math.min(Math.max(startCol,0),length);
		endCol = Math.max(Math.min(endCol,length),startCol);
		return new String(text,startCol,endCol-startCol);
	}

	/**
	 * @return number of columns on this line
	 */
	public int length() {
		return length;
	}

	public int getWordStart(int col) {
		return ASUtil.getTokenStart(text,col,getExtraWordChars());
	}

	public int getWordEnd(int col) {
		return ASUtil.getTokenEnd(text,col,getExtraWordChars());
	}

	/**
	 * @param col
	 * @return the index of the token at the specified column, or -1 if the line is empty
	 */
	public int getTokenAt(int col) {
		for (int i=0;i<nReferences-1;++i) {
			if (referenceIndices[i+1]>col) {
				return i;
			}
		}
		return nReferences-1;
	}
	
	/**
	 * @param col
	 * @return the {@link Symbol} at the specified column or null if none
	 */
	public Symbol getSymbolAt(int col) {
		int index = getTokenAt(col);
		if (index>=0) {
			Object object = references[index];
			if (object!=null && (object instanceof Symbol)) {
				return (Symbol)object;
			}
		}
		return null;
	}
	/**
	 * @param col
	 * @return the {@link SyntaxError} at the specified column or null if none
	 */
	public SyntaxError getErrorAt(int col) {
		if (errors!=null) {
			for (int i=0;i<nErrors;++i) {
				SyntaxError error = errors[i];
				// NOTE: There really shouldn't have to be a check for null here, but it showed up once, so better safe than sorry.
				if (error!=null) {
					if (error.getStartCol()<=col && error.getEndCol()>=col) {
						return error;
					}
				}
			}
		}
		return null;
	}

	public char[] getExtraWordChars() {
		return DEFAULT_EXTRA_WORD_CHARS;
	}

	/**
	 * Replaces the text from startCol to endCol with the specified text
	 * @param startCol
	 * @param endCol
	 * @param text
	 * @return an object representing the event so that it can be reversed
	 */
	public abstract TextReplacementEvent replace(int startCol,int endCol,String text);

	public TextReplacementEvent delete(int startCol,int endCol) {
		return replace(startCol,endCol,"");
	}

	public TextReplacementEvent insert(int col,String text) {
		return replace(col,col,text);
	}

	public TextReplacementEvent append(String text) {
		return replace(length,length,text);
	}

	public TextReplacementEvent prepend(String text) {
		return replace(0,0,text);
	}

	/**
	 * Compacts the arrays in this Line, removing the excess space
	 */
	public void trimToSize() {
		if (text.length>length) {
			text = ArrayUtil.subarray(text,0,length);
		}
		if (nReferences==0 && references!=null) {
			references = null;
			referenceIndices = null;
			isDefiningReference = null;
		}
		else if (references!=null && references.length>nReferences) {
			references = ArrayUtil.subarray(references,0,nReferences);
			referenceIndices = ArrayUtil.subarray(referenceIndices,0,nReferences);
			isDefiningReference = ArrayUtil.subarray(isDefiningReference,0,nReferences);
		}
		if (nErrors==0 && errors!=null) {
			errors = null;
		}
		else if (errors!=null && errors.length>nErrors) {
			errors = (SyntaxError[])ArrayUtil.subarray(errors,0,nErrors);
		}
	}

	protected void ensureCapacity(int minLength) {
		if (text.length<minLength) {
			int newLength = Math.max(2*text.length,minLength);
			char[] newText = new char[newLength];
			System.arraycopy(text,0,newText,0,length);
			text = newText;
		}
	}

	protected void ensureRefCapacity(int minLength) {
		if (references==null) {
			references = new Object[minLength];
			referenceIndices = new int[minLength];
			isDefiningReference = new boolean[minLength];
		}
		else if (references.length<minLength) {
			int newLength = Math.max(2*references.length,minLength);
			Object[] newReferences = new Object[newLength];
			int[] newReferenceIndices = new int[newLength];
			boolean[] newIsDefiningReference = new boolean[newLength];
			System.arraycopy(references,0,newReferences,0,nReferences);
			System.arraycopy(referenceIndices,0,newReferenceIndices,0,nReferences);
			System.arraycopy(isDefiningReference,0,newIsDefiningReference,0,nReferences);
			references = newReferences;
			referenceIndices = newReferenceIndices;
			isDefiningReference = newIsDefiningReference;
		}
	}

	protected void ensureErrorCapacity(int minLength) {
		if (errors==null) {
			errors = new SyntaxError[minLength];
		}
		else if (errors.length<minLength) {
			int newLength = Math.max(2*errors.length,minLength);
			SyntaxError[] newErrors = new SyntaxError[newLength];
			System.arraycopy(errors,0,newErrors,0,nErrors);
			errors = newErrors;
		}
	}

	public void addError(SyntaxError error) {
		ensureErrorCapacity(nErrors+1);
		errors[nErrors++] = error;
//		list.getParent().getProject().getErrors().add(error);
	}

	public void addReference(Object reference,int index,boolean isDefining) {
		ensureRefCapacity(nReferences+1);
		references[nReferences] = reference;
		referenceIndices[nReferences] = index;
		isDefiningReference[nReferences] = isDefining;
		++nReferences;
	}

	/**
	 * Copies this line, possibly giving the new line a different {@link LineList}
	 * @param list either this Line's {@link LineList}, or a new one
	 * @param index the index at which the copied line will be placed later
	 * @return the new Line
	 */
	public abstract Line copy(LineList list,int index);

	/**
	 * Constructor for a Line
	 */
	protected Line(LineList list,char[] text) {
		this.list = list;
		this.text = text;
		length = text.length;
		references = null;
		referenceIndices = null;
		isDefiningReference = null;
		nReferences = 0;
		errors = null;
		nErrors = 0;
	}

	public boolean isOperator(int index,char c) {
		return (index<nReferences && references[index]==OPERATOR_MARKER && text[referenceIndices[index]]==c && (index+1==nReferences || referenceIndices[index+1]-referenceIndices[index]==1));
	}

	public int getTokenColumn(int index) {
		return (index>=nReferences) ? length : (referenceIndices!=null) ? referenceIndices[index] : 0;
	}

	public Object[] getReferences() {
		return references;
	}
	public int getNumReferences() {
		return nReferences;
	}

	public int getNumErrors() {
		return nErrors;
	}

	public static String tabsToSpaces(String text,int column) {
		for (int i=0;i<text.length();++i) {
			if (text.charAt(i)=='\t') {
				text = text.substring(0,i)+SPACES_FOR_TAB[(i+column)&3]+text.substring(i+1);
			}
		}
		return text;
	}

	public SyntaxError[] getErrors() {
		return errors;
	}

	public LineList getList() {
		return list;
	}

	public char[] getText() {
		return text;
	}

	/**
	 * Clears the errors on the current line.
	 * NOTE: This must be called on all {@link Line}s of things removed from the current project (e.g. {@link model.code.Function}s that are cut and not yet pasted).
	 * If it isn't, the {@link model.Project}'s error list may have extra errors in it.
	 * This also means that functions such as {@link ASMLine#updateErrors()} must be called when being readded. 
	 */
	public void clearErrors() {
//		if (nErrors>0) {
//			Set<SyntaxError> projectErrors = list.getParent().getProject().getErrors();
//			for (int i=0;i<nErrors;++i) {
//				projectErrors.remove(errors[i]);
//			}
			nErrors = 0;
//		}
	}

	public void clearReferences() {
		if (nReferences>0) {
			// TODO: Update the backward reference lists here once implemented.
			for (int i=0;i<nReferences;++i) {

			}
			nReferences = 0;
		}
	}

	protected void replaceJustText(int start,int end,char[] newText) {
		int sizeDifference = newText.length-(end-start);
		boolean isAtEnd = start==length;
		// Prepare the space into which the new text will be copied.
		if (sizeDifference!=0) {
			ensureCapacity(length+sizeDifference);
			if (end<length) {
				System.arraycopy(text,end,text,start+newText.length,length-end);
			}
			length += sizeDifference;
		}
		// Copy in the new text.
		System.arraycopy(newText,0,text,start,newText.length);
		// Adjust referenceIndices and error locations if necessary.
		if (sizeDifference!=0 && !isAtEnd) {
			for (int i=0;i<nReferences;++i) {
				if (referenceIndices[i]>=end) {
					referenceIndices[i] += sizeDifference;
				}
			}
			for (int i=0;i<nErrors;++i) {
				if (errors[i].getStartCol()>=end) {
					errors[i].setLocation(errors[i].getStartCol()+sizeDifference,errors[i].getEndCol()+sizeDifference);
				}
			}
		}
	}

	/**
	 * Compares the text representations of this and that ignoring case
	 * @param that the second operand of the comparison
	 * @return negative when this is before that, zero when this is that, and positive when this is after that
	 */
	public int compareTo(Object that) {
		String thisText = toString();
		String thatText = that.toString();
		return thisText.compareToIgnoreCase(thatText);
	}

	public int[] getReferenceIndices() {
		return referenceIndices;
	}

	public boolean[] getIsDefiningReference() {
		return isDefiningReference;
	}

	public boolean equals(Object o) {
		if (!(o instanceof Line) && !(o instanceof String) && !(o instanceof StringBuffer) && !(o instanceof ObservedString)) {
			return false;
		}
		if (this==o) {
			return true;
		}
		char[] thatValue;
		if (o instanceof Line) {
			if (length!=((Line)o).length) {return false;}
			thatValue = ((Line)o).text;
		}
		else if (o instanceof String) {
			if (length!=((String)o).length()) {return false;}
			thatValue = ((String)o).toCharArray();
		}
		else if (o instanceof ObservedString) {
			if (length!=((ObservedString)o).length()) {return false;}
			thatValue = ((ObservedString)o).toCharArray();
		}
		else { // o instanceof StringBuffer
			if (length!=((StringBuffer)o).length()) {return false;}
			thatValue = o.toString().toCharArray();
		}
		return ASUtil.equals(text,thatValue,length);
	}

	public int indexOf(String query,int col) {
		while (col<=length-query.length()) {
			int i=0;
			while (i<query.length() && text[col+i]==query.charAt(i)) {
				++i;
			}
			if (i==query.length()) {
				return col;
			}
			++col;
		}
		return -1;
	}

	public int lastIndexOf(String query, int col) {
		col = Math.min(col,length-query.length());
		while (col>=0) {
			int i=0;
			while (i<query.length() && text[col+i]==query.charAt(i)) {
				++i;
			}
			if (i==query.length()) {
				return col;
			}
			--col;
		}
		return -1;
	}
}
